package com.nado.extensions.util;

import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.Stack;
import java.io.IOException;
import java.io.EOFException;
import java.io.Reader;

/**
 * simple xml parser (revised original implementation by casi, just for fun)
 *
 */
public abstract class XMLParser<T extends XMLElement<T>> {

	protected Logger logger = Logger.getLogger(XMLParser.class.getName());

	protected Reader reader;
	protected Stack<T> elements;

	public XMLParser() {
		elements = new Stack<T>();
	}

	public T parse(Reader reader) throws IOException {
		this.reader = reader;

		// skip xml declaration or DocTypes
		skipPrologs();

		T last = null;
		try {
			while (true) {
				// remove the prepend or trailing white spaces
				Result result = readNode();
				if (logger.isLoggable(Level.FINE)) {
					logger.fine(" [XMLParser/parse] " + result.value);
				}
				switch (result.TYPE) {
					case COMMENT:
						break;
					case TAG:
						last = handleTag(result);
						break;
					case TEXT:
						if (!elements.isEmpty()) {
							elements.peek().value(result.value.trim());
						}
						break;
				}
			}
		} catch (EOFException e) {
			// completed
		}
		if (!elements.isEmpty()) {
			throw new IOException("not closed tags " + elements);
		}
		return last;
	}

	protected T handleTag(Result result) throws IOException {
		String tagString = result.value;
		if (tagString.startsWith("/")) {
			// close tag
			return handleCloseTag(tagString);
		}
		// open tag or tag with both open and close tags
		boolean closed = tagString.endsWith("/");
		CharSequence sequence = new CharSequence(tagString);

		CURSOR cursor;
		do {
			cursor = traverse(sequence);
		} while (cursor != CURSOR.KEY);

		T element = newElement(sequence.value());

		String key = null;
		do {
			cursor = traverse(sequence);
			if (logger.isLoggable(Level.FINE)) {
				logger.fine(" [XMLParser/handleTag] " + cursor + " => " + sequence.value());
			}
			if (cursor == CURSOR.VALUE) {
				element.attribute(key, sequence.valueNotQuoted());
			}
			if (cursor == CURSOR.KEY) {
				key = sequence.value();
			}
		} while(cursor != CURSOR.END);
		// read the text between the open and close tag
		if (!elements.isEmpty()) {
			// add new element as a child element of the current element
			elements.peek().append(element.name(), element);
		}
		if (!closed) {
			elements.push(element);
		}
		return element;
	}

	protected abstract T newElement(String tagName);

	protected T handleCloseTag(String currentTag) throws IOException {
		String tagName = currentTag.substring(1);

		if (elements.isEmpty()) {
			// no open tag
			throw new IOException("Got close tag '" + tagName + "' without open tag");
		}
		T current = elements.pop();
		// close tag does not match with open tag
		if (!tagName.equals(current.name())) {
			throw new IOException("Expected close tag for '" + current.name() + "' but got '" + tagName);
		}
		return current;
	}

	protected char read() throws IOException {
		int result = reader.read();
		if (result < 0) {
			throw new EOFException();
		}
		return (char) result;
	}

	protected char peek() throws IOException {
		reader.mark(1);
		try {
			return read();
		} finally {
			reader.reset();
		}
	}

	protected void peek(int[] buffer) throws IOException {
		reader.mark(buffer.length);
		for (int i=0; i<buffer.length; i++) {
			buffer[i] = reader.read();
		}
		reader.reset();
	}

	protected void skipWhitespace() throws IOException {
		while (reader.ready() && Character.isWhitespace(peek())) {
			reader.read();
		}
	}

	protected void skipProlog() throws IOException {
		// skip "<?" or "<!"
		reader.skip(2);

		while (true) {
			int next = peek();

			if (next == '>') {
				reader.read();
				break;
			} else if (next == '<') {
				// nesting prolog
				skipProlog();
			} else {
				reader.read();
			}
		}
	}

	protected void skipPrologs() throws IOException {
		while (true) {
			skipWhitespace();

			int[] next = new int[2];
			peek(next);

			if (next[0] != '<') {
				throw new IOException("Expected '<' but got '" + (char)next[0] + "'.");
			}

			if ((next[1] == '?') || (next[1] == '!')) {
				skipProlog();
			} else {
				break;
			}
		}
	}

	protected Result readNode() throws IOException {
		skipWhitespace();

		StringBuilder sb = new StringBuilder();

		if (read() != '<') {
			// simple text
			reader.reset();
			while (peek() != '<') {
				sb.append(read());
			}
			return new Result(NODE_TYPE.TEXT, sb.toString());
		}
		reader.mark(1);
		if (read() != '!') {
			// tag element
			reader.reset();
			while (peek() != '>') {
				sb.append(read());
			}
			if (!isNext(">")) {
				throw new IOException("no tag closure");
			}
			return new Result(NODE_TYPE.TAG, sb.toString());
		}
		if (isNext("[CDATA[")) {
			// CDATA text
			while (true) {
				while (peek() != ']') {
					sb.append(read());
				}
				if (isNext("]]")) {
					break;
				}
				sb.append(read());      // consume ']'
			}
			return new Result(NODE_TYPE.TEXT, sb.toString());
		}
		// skip comment
		while (peek() != '>') {
			sb.append(read());
		}
		return new Result(NODE_TYPE.COMMENT, sb.toString());
	}

	protected class Result {
		NODE_TYPE TYPE;
		String value;
		Result(NODE_TYPE TYPE, String value) {
			this.TYPE = TYPE;
			this.value = value;
		}
	}

	protected boolean isNext(String string) throws IOException {
		reader.mark(string.length());
		for (char expected : string.toCharArray()) {
			if (read() != expected) {
				reader.reset();
				return false;
			}
		}
		return true;
	}

	protected CURSOR traverse(CharSequence tagValue) {
		boolean valueStart = false;
		for (tagValue.reset(); !tagValue.complete(); tagValue.proceed()) {
			char value = tagValue.current();
			switch(value) {
				case '<':
				case '>':
				case '/':
					if (tagValue.trailing()) {
						tagValue.skip();
						continue;
					}
					return CURSOR.KEY;
				case ' ':
					if (tagValue.trailing()) {
						tagValue.skip();
						continue;
					} else if (valueStart) {
						continue;
					}
					return CURSOR.KEY;
				case '=':
					if (tagValue.trailing()) {
						tagValue.proceed();     // part of value
						return CURSOR.SEPARATOR;
					}
					return CURSOR.KEY;
				case '\'':
					if (valueStart) {
						tagValue.proceed();     // part of value
						return CURSOR.VALUE;
					}
					valueStart = true;
			}
		}
		return tagValue.trailing() ? CURSOR.END : CURSOR.KEY;
	}

	protected static class CharSequence {
		protected int index;
		protected int offset;
		protected String original;
		protected CharSequence(String original) {
			this.original = original;
		}
		protected char current() {
			return original.charAt(index);
		}
		protected void proceed() {
			index++;
		}
		protected void skip() {
			offset++;
		}
		protected boolean trailing() {
			return offset == index;
		}
		protected boolean complete() {
			return original.length() <= index;
		}
		protected String value() {
			return original.substring(offset, index);
		}
		protected String valueNotQuoted() {
			return original.substring(offset + 1, index - 1);
		}
		protected void reset() {
			offset = index;
		}
	}

	protected enum NODE_TYPE {
		TEXT, TAG, COMMENT
	}

	protected static enum CURSOR {
		KEY, SEPARATOR, VALUE, END
	}
}